%%% Copyright 2010 Google Inc.
%%%
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%%     http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.

%%% @copyright 2010 Google Inc. All Rights Reserved.
%%% @author <jacoby@google.com> Jacoby Thwaites [http://sites/madcloud]
%%% @version {@version}
%%% @doc Tests for `prog:' module.
%%% ==Beware!==
%%% These tests delete existing data. So make sure you execute them on a
%%% separate test node, as described here - <em>not a node with data
%%% you care about!</em>
%%%
%%% To run tests, start up a test node in the /ebin directory
%%% holding the .beam files. In the example below, we're giving the node
%%% the shortname `test'.
%%%
%%% `erl -name test'
%%%
%%% Then run the tests from the shell:
%%%
%%% `eunit:test(prog)'
%%%
%%% There are one or two useful exported functions for interactive setup
%%% and testing as well.
%%%
%%% @TODO(jacoby) Add tests for creation of .. entry in subfolders of
%%% both folders and prog/ops.
-module(prog_tests).

%% Include eunit and txm headers
-include_lib("eunit/include/eunit.hrl").
-include("txm.hrl").

%% Test group with setup and teardown of Mnesia.
big_test_() ->

  % Tests depend on preceding tests.
  {inorder,

   % This tuple specifies a setup fun(), teardown fun() and list of test fun().
   {setup,
    % Setup creates new schema and tables.
    fun() ->
         mnesia:stop(),
         mnesia:delete_schema([node()]),
         prog:create_tables()
    end,

    % Cleanup deletes schema.
    fun(_) -> mnesia:stop(),
              mnesia:delete_schema([node()])
    end,

    % Test functions grouped by area.
    [
      tree_tests(),
      field_tests(),
      op_tests(),
      snapshot_tests(),
      contents_tests(),
      error_tests(),
      scope_tests(),
      exception_tests(),
      delete_tests(),
      import_tests()
      ]}}.


%% Returns a list of functions that test folder and tree functionality.
tree_tests() ->
  [
    % Checks root folder reference.
    fun() ->
         ?assertMatch({ok, {folder, {0, 0, 0}}},
                      prog:get_root_ref()),
         ?assertMatch({ok, {folder, {0, 0, 0}}},
                      prog:resolve_absolute("/"))
    end,

    % Checks root folder retrieval by pathname.
    fun() ->
         {ok, Ref} = prog:resolve_absolute("/"),
         ?assertMatch({ok, #folder{}},
                      prog:get_record(Ref))
    end,

    % Checks toplevel folder creation, parent specified by reference.
    fun() ->
         ?assertMatch({ok, #folder{}},
                      prog:new_record({folder, {0, 0, 0}}, "com", #folder{})),
         ?assertMatch({ok, #folder{}},
                      prog:new_record({folder, {0, 0, 0}}, "org", #folder{}))
    end,

    % Checks toplevel folder retrieval by pathname.
    fun() ->
         {ok, Ref} = prog:resolve_absolute("/com"),
         ?assertMatch({ok, #folder{}}, prog:get_record(Ref))
    end,

    % Checks subfolder creation by name and retrieval by reference.
    fun() ->
         {ok, F1} = prog:resolve_absolute("/com"),
         {ok, F2} = prog:new_record(F1, "google", #folder{}),
         Ref = prog:record_to_ref(F2),
         ?assertMatch({ok, F2}, prog:get_record(Ref))
    end,

    % Checks subfolder retrieval by pathname.
    fun() ->
         {ok, F} = prog:resolve_absolute("/com/google"),
         ?assertMatch({ok, #folder{}}, prog:get_record(F))
    end
  ].

%% Returns a list of functions that test field creation and retrieval.
field_tests() ->
  [
    % Checks field creation in 2 subfolders.
    fun() ->
         {ok, F1} = prog:resolve_absolute("/com/google"),
         ?assertMatch({ok, #field{}},
                      prog:new_record(F1, "field1", #field{})),
         {ok, F2} = prog:resolve_absolute("/org"),
         ?assertMatch({ok, #field{}},
                      prog:new_record(F2, "field2", #field{}))
    end,

    % Checks field retrieval by pathname.
    fun() ->
         {ok, F} = prog:resolve_absolute("/com/google/field1"),
         ?assertMatch({ok, #field{}},
                      prog:get_record(F))
    end,

    % Checks field retrieval by reference.
    fun() ->
         {ok, F} = prog:resolve_absolute("/com/google/field1"),
         {ok, #field{} = Field} =
           prog:get_record(F),
         Ref = prog:record_to_ref(Field),
         ?assertMatch({ok, Field}, prog:get_record(Ref))
    end
  ].

%% Returns a list of functions that test operation creation and retrieval.
op_tests() ->
  [
    % Checks op creation in a subfolder
    fun() ->
         {ok, F1} = prog:resolve_absolute("/com/google/field1"),
         {ok, F2} = prog:resolve_absolute("/org/field2"),
         Mask = nr_set:from_proplist(
                  [{"five", F1}]),
         Input = nr_set:from_proplist(
                   [{"one", F1},
                    {"two", F2}]),

         Outputs = nr_set_dict:from_proplist(
                     [{"Either",
                       [{"one", F1},
                        {"two", F2}]},
                      {"Or",
                       [{"five", F1}]}]),

         Op = #op{mask = Mask,
                  input = Input,
                  outputs = Outputs},

         {ok, P} = prog:resolve_absolute("/com/google"),
         ?assertMatch({ok, #op{}},
                      prog:new_record(P, "op1", Op))
    end,

    % Checks op retrieval by name
    fun() ->
         {ok, O} = prog:resolve_absolute("/com/google/op1"),
         ?assertMatch({ok, #op{}}, prog:get_record(O))
    end,

    % Checks composite op creation in a subfolder
    fun() ->
         {ok, F1} = prog:resolve_absolute("/com/google/field1"),
         {ok, F2} = prog:resolve_absolute("/org/field2"),
         Mask = nr_set:from_proplist(
                  [{"five", F1},
                   {"three", F2}]),
         Input = nr_set:from_proplist(
                   [{"one", F1},
                    {"two", F2}]),
         Outputs = nr_set_dict:from_proplist(
                     [{"Either",
                       [{"three", F1},
                        {"four", F2}]},
                      {"Or",
                       [{"five", F1}]}]),

         Op = #op{isComposite = true,
                  mask = Mask,
                  input = Input,
                  outputs = Outputs},
         {ok, P} = prog:resolve_absolute("/com/google"),
         ?assertMatch({ok, #op{}},
                      prog:new_record(P, "op2", Op))
    end,

    % Checks composite op root folder retrieval
    fun() ->
         {ok, O} = prog:resolve_absolute("/com/google/op2"),
         ?assertMatch({ok, {folder, {_, _, _}}},
                      prog:get_root_ref(O))
    end,

    % Checks composite op subtree creation by creating /user/jacoby/subOp
    % under the op /com/google/op2.
    fun() ->
         {ok, O} = prog:resolve_absolute("/com/google/op2"),
         {ok, Ref} = prog:get_root_ref(O),
         {ok, UserFolder} = prog:new_record(Ref, "user", #folder{}),
         UserFolderRef = prog:record_to_ref(UserFolder),
         {ok, JacobyFolder} =
            prog:new_record(UserFolderRef, "jacoby", #folder{}),
         JacobyFolderRef = prog:record_to_ref(JacobyFolder),
         ?assertMatch({ok, #op{}},
                      prog:new_record(JacobyFolderRef, "subOp", #op{}))
    end,

    % Checks composite op subtree retrieval.
    fun() ->
        {ok, O1} = prog:resolve_absolute("/com/google/op2"),
        {ok, Op2RootRef} = prog:get_root_ref(O1),
        {ok, O2} = prog:resolve_relative(Op2RootRef, "user/jacoby/subOp"),
        ?assertMatch({ok, #op{}}, prog:get_record(O2))
    end
  ].

%% Returns a list of functions that test snapshotting
snapshot_tests() ->
  [
    % Checks snapshot creation of composite op
    fun() ->

         % First snapshot.
         {ok, O} = prog:resolve_absolute("/com/google/op2"),
         ?assertMatch({ok, #op{}},
                      prog:snapshot(O, "First version")),
         ?assertMatch({ok, {folder, {_, _, _}}},
                      prog:get_root_ref(O, 1)),

         % Second snapshot.
         prog:snapshot(O),
         ?assertMatch({ok, {folder, {_, _, _}}},
                      prog:get_root_ref(O, 2))
    end,

    % Check snapshot retrieval by name
    fun() ->
         {ok, O} = prog:resolve_absolute("/com/google/op2"),
         ?assertEqual(prog:get_root_ref(O, 1),
                      prog:get_root_ref(O, "First version"))
    end,

    % Check snapshots have copied content correctly
    fun() ->
         {ok, O} = prog:resolve_absolute("/com/google/op2"),
         {ok, V1Root} = prog:get_root_ref(O, 1),
         {ok, V2Root} = prog:get_root_ref(O, 2),
         ?assertNot(V1Root =:= V2Root),

         {ok, O1} = prog:resolve_relative(V1Root, "user/jacoby/subOp"),
         {ok, O2} = prog:resolve_relative(V2Root, "user/jacoby/subOp"),
         {ok, Op1} = prog:get_record(O1),
         {ok, Op2} = prog:get_record(O2),
         ?assertEqual(Op1, Op2)
    end
  ].

%% Returns a list of functions that test the get_contents function.
%% This returns a tuple of {nr_set(), list(rec())} where the nr_set()
%% is an index by name into the list of records.
contents_tests() ->
  [
    % Checks shallow index.
    fun() ->
         {ok, F} = prog:resolve_absolute("/com/google"),
         {ok, Index} = prog:get_shallow_index(F),
         {index, Props, Structs} = Index,

         % Check props has id defined.
         ?assert(proplists:is_defined(id, Props)),

         % Check index has exactly 1 field, 2 op and 1 folder entries.
         Count = lists:foldl(fun(Struct, Acc) ->
                            case Struct of
                              {field, _Attributes} -> Acc + 1;
                              {op, _Attributes} -> Acc + 10;
                              {folder, _Attributes} -> Acc + 100
                            end
                       end, 0, Structs),
         ?assertEqual(121, Count)
     end,

    % Checks shallow contents.
    fun() ->
         {ok, F} = prog:resolve_absolute("/com/google"),
         {ok, Contents} = prog:get_shallow_contents(F),

         % Check contents has exactly 1 field and 2 op records.
         Count = lists:foldl(fun(Record, Acc) ->
                                  case Record of
                                    #?FIELD{} -> Acc + 1;
                                    #?OP{} -> Acc + 10
                                  end
                             end, 0, Contents),
        ?assertEqual(21, Count)
    end,

    % Checks deep index.
    fun() ->
         {ok, F} = prog:resolve_absolute("/com"),
         {ok, Index} = prog:get_deep_index(F),

         % Check index has 1 nested entry.
         ?assertMatch({index, _, [{index, _, _}]}, Index),

         % Check props has id defined.
         {index, Props1, [{index, Props2, Contents}]} = Index,
         ?assert(proplists:is_defined(id, Props1)),
         ?assert(proplists:is_defined(id, Props2)),

         % Check nested index has exactly 1 field and 2 op records.
         Count = lists:foldl(fun(Record, Acc) ->
                                  case Record of
                                    {field, _Attributes} -> Acc + 1;
                                    {op, _Attributes} -> Acc + 10
                                    end
                             end, 0, Contents),
         ?assertEqual(21, Count)
    end,

    % Checks deep contents.
    fun() ->
         {ok, F} = prog:resolve_absolute("/"),
         {ok, Records} = prog:get_deep_contents(F),

         Count = lists:foldl(fun(Record, Acc) ->
                                  case Record of
                                    #?FIELD{} -> Acc + 1;
                                    #?OP{} -> Acc + 10
                                    end
                             end, 0, Records),
         ?assertEqual(22, Count)
    end
  ].

%% Returns a list of functions that test error returns.
error_tests() ->
  [
    % Checks error returns on non-present IDs
    fun() ->
         ?assertMatch({error, not_found},
                      prog:get_record({folder, {3, 2, 1}})),
         ?assertMatch({error, not_found},
                      prog:get_record({field, {3, 2, 1}})),
         ?assertMatch({error, not_found},
                      prog:get_record({op, {3, 2, 1}}))
    end,

    % Checks error return on incorrect path
    fun() ->
         ?assertMatch({error, not_found},
                      prog:resolve_absolute("/com/goggle/txm"))
    end,

    % Checks error return on incorrect name but correct path
    fun() ->
         ?assertMatch({error, not_found},
                      prog:resolve_absolute("/com/goggle")),
         ?assertMatch({error, not_found},
                      prog:resolve_absolute("/com/google/badField"))
    end
  ].

%% Returns a list of functions that tested scoped name references.
scope_tests() ->
  [

    % Checks scoped name resolves in given folder.
    fun() ->
         {ok, F1} = prog:resolve_absolute("/com/google"),
         {ok, O1} = prog:resolve_absolute("/com/google/op2"),
         ?assertMatch({ok, O1},
                      prog:resolve_scoped(F1, "op2"))
    end,

    % Checks scoped name resolves to parent.
    fun() ->
         {ok, F1} = prog:resolve_absolute("/com/google"),
         {ok, F2} = prog:resolve_absolute("/com"),
         ?assertMatch({ok, F2},
                      prog:resolve_scoped(F1, "com"))
    end,

    % Checks scoped name resolves to ancestor of composite op root.
    fun() ->
         {ok, O} = prog:resolve_absolute("/com/google/op2"),
         {ok, {folder, {_, _, _}} = OpRoot} =
           prog:get_root_ref(O, 1),
         {ok, {folder, {_, _, _}} = F1} =
           prog:resolve_relative(OpRoot, "user/jacoby"),
         {ok, {field, {_, _, _}} = F2} =
           prog:resolve_absolute("/com/google/field1"),
         ?assertMatch({ok, F2},
                      prog:resolve_scoped(F1, "field1"))
    end
  ].

%% Returns a list of functions that test exceptions are thrown correctly.
exception_tests() ->
  [
    % Checks exception upon attempt to create with one bad arg type
    fun() ->
         ?assertException(error, _,
           prog:new_record(?ROOT_FOLDER_REF, "goodName", badRecord)),
         ?assertException(error, _,
           prog:new_folder(?ROOT_FOLDER_REF, badName, #folder{})),
         ?assertException(error, _,
           prog:new_folder(badId, "goodName", #folder{}))
    end,


    % Checks exception upon attempt to retrieve with bad arg type
    fun() ->
         ?assertException(error, _, prog:get_record(someAtom))
    end
  ].

%% Returns a list of functions that test deletion.
delete_tests() ->
  [
    % Checks delete_ref works correctly.
    fun() ->
         ?assertMatch({ok, _Ref},
                      prog:resolve_absolute("/com/google/op2")),
         {ok, F} = prog:resolve_absolute("/com/google"),
         ?assertEqual(ok,
                      prog:delete_ref(F, "op2")),
         ?assertMatch({error, not_found},
                      prog:resolve_absolute("/com/google/op2"))
    end
  ].

%% Returns a list of functions that test XML import.
import_tests() ->
  [
    % Check file import succeeds.
    fun() ->
         ?assertMatch(ok,
                      prog:import_file({folder,{0,0,0}},
                                       "../test/import_test.xml"))
    end,

    % Check import placed correct entries in the prog db.
    fun() ->
         ?assertMatch({ok, {field, _Id1}},
                      prog:resolve_absolute("/F1")),
         ?assertMatch({ok, {field, _Id2}},
                      prog:resolve_absolute("/F2")),
         ?assertMatch({ok, {field, _Id3}},
                      prog:resolve_absolute("/B/F3")),
         ?assertMatch({ok, {op, _Id4}},
                      prog:resolve_absolute("/B/O"))
         end,

    % Check import placed correct op record with resolved field names.
    fun() ->
         {ok, F1Ref} = prog:resolve_absolute("/F1"),
         {ok, F2Ref} = prog:resolve_absolute("/F2"),
         {ok, F3Ref} = prog:resolve_absolute("/B/F3"),
         {ok, OpRef} = prog:resolve_absolute("/B/O"),
         {ok, Op} = prog:get_record(OpRef),

         % Check mask set.
         Mask = Op#?OP.mask,
         ?assertEqual(1, nr_set:count(Mask)),
         ?assertEqual(F1Ref, nr_set:ref_for_name("F1", Mask)),

         % Check input set.
         Input = Op#?OP.input,
         ?assertEqual(1, nr_set:count(Input)),
         ?assertEqual(F2Ref, nr_set:ref_for_name("F2", Input)),

         % Check output sets.
         Outputs = Op#?OP.outputs,
         ?assertEqual(2, nr_set_dict:count_sets(Outputs)),
         O1 = nr_set_dict:get_nr_set("O1", Outputs),
         ?assertEqual(2, nr_set:count(O1)),
         O2 = nr_set_dict:get_nr_set("O2", Outputs),
         ?assertEqual(3, nr_set:count(O2)),
         ?assertEqual(F2Ref, nr_set:ref_for_name("F2", O1)),
         ?assertEqual(F3Ref, nr_set:ref_for_name("F3", O2))
    end

  ].
